package dao

import (
	"context"
	"errors"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"time"
)

//go:generate mockgen -source=./article.go -package=daomocks -destination=./mocks/article.mock.go ArticleDAO
type ArticleDao interface {
	Insert(ctx context.Context, art Article) (int64, error)
	UpdateById(ctx context.Context, entity Article) error
	Sync(ctx context.Context, entity Article) (int64, error)
	SyncStatus(ctx context.Context, uid int64, id int64, status uint8) error
	GetByAuthor(ctx context.Context, uid int64, offset int, limit int) ([]Article, error)
	GetById(ctx context.Context, id int64) (Article, error)
	GetPubById(ctx context.Context, id int64) (PublishedArticle, error)
	ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]PublishedArticle, error)
}

func NewArticleORMDAO(db *gorm.DB) ArticleDao {
	return &ArticleORMDAO{
		db: db,
	}
}

type ArticleORMDAO struct {
	db *gorm.DB
}

func (a *ArticleORMDAO) ListPub(ctx context.Context, start time.Time, offset int, limit int) ([]PublishedArticle, error) {
	var res []PublishedArticle
	const ArticleStatusPublished = 2
	err := a.db.WithContext(ctx).
		Where("utime < ? AND status = ?",
			start.UnixMilli(), ArticleStatusPublished).
		Offset(offset).Limit(limit).
		First(&res).Error
	return res, err
}

func (a *ArticleORMDAO) GetPubById(ctx context.Context, id int64) (PublishedArticle, error) {
	var art PublishedArticle
	err := a.db.WithContext(ctx).Where("id=?", id).First(&art).Error
	return art, err
}

func (a *ArticleORMDAO) GetById(ctx context.Context, id int64) (Article, error) {
	var art Article
	err := a.db.WithContext(ctx).Where("id=?", id).First(&art).Error
	return art, err
}

func (a *ArticleORMDAO) GetByAuthor(ctx context.Context, uid int64, offset int, limit int) ([]Article, error) {
	var arts []Article
	err := a.db.WithContext(ctx).Where("author_id = ?", uid).Offset(offset).
		Limit(limit).Order("utime DESC").Find(&arts).Error
	return arts, err
}

func (a *ArticleORMDAO) Insert(ctx context.Context, art Article) (int64, error) {
	now := time.Now().UnixMilli()
	art.Ctime = now
	art.Utime = now
	err := a.db.WithContext(ctx).Create(&art).Error
	return art.Id, err //会把id填上
}
func (a *ArticleORMDAO) UpdateById(ctx context.Context, art Article) error {
	now := time.Now().UnixMilli()
	res := a.db.Model(&Article{}).WithContext(ctx).
		Where("id=? AND author_id = ? ", art.Id, art.AuthorId).
		Updates(map[string]any{
			"title":   art.Title,
			"content": art.Content,
			"status":  art.Status,
			"utime":   now,
		})
	err := res.Error
	if err != nil {
		return err
	}
	if res.RowsAffected == 0 {
		return errors.New("更新数据失败")
	}
	return nil
}

// 闭包的写法
func (a *ArticleORMDAO) Sync(ctx context.Context, artn Article) (int64, error) {
	var id = artn.Id
	var err error
	err = a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		dao := NewArticleORMDAO(tx) //使用事务实现创建我自己
		if id > 0 {
			err = dao.UpdateById(ctx, artn)
		} else {
			id, err = dao.Insert(ctx, artn)
		}
		if err != nil {
			return err
		}
		artn.Id = id
		now := time.Now().UnixMilli()
		pubArt := PublishedArticle(artn)
		pubArt.Ctime = now
		pubArt.Utime = now
		err = tx.Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "id"}}, //对mysql不起效 可以兼容别的方言
			//INSERT XXX ON DUPLICATE KEY SET `title`=?
			//别的方言 sqlite INSERT XXX ON CONFLICT DO UPDATES
			//冲突了更新什么
			DoUpdates: clause.Assignments(map[string]interface{}{
				"title":   pubArt.Title,
				"content": pubArt.Content,
				"utime":   now,
				"status":  pubArt.Status,
			}),
		}).Create(&pubArt).Error
		return err
	})
	return id, err
}
func (a *ArticleORMDAO) SyncStatus(ctx context.Context, uid int64, id int64, status uint8) error {
	now := time.Now().UnixMilli()
	return a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		res := tx.Model(&Article{}).Where("id = ? and author_id = ?", id, uid).
			Updates(map[string]any{
				"utime":  now,
				"status": status,
			})
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected != 1 {
			return errors.New("更新数据失败 ID不对或创作者不对")
		}
		return tx.Model(&PublishedArticle{}).Where("id = ?", id).
			Updates(map[string]any{
				"utime":  now,
				"status": status,
			}).Error
	})
}
func (a *ArticleORMDAO) SyncV1(ctx context.Context, artn Article) (int64, error) {
	tx := a.db.WithContext(ctx).Begin()
	if tx.Error != nil {
		return 0, tx.Error
	}
	//防止后面业务panic
	defer tx.Rollback()

	dao := NewArticleORMDAO(tx) //使用事务实现创建我自己

	var id = artn.Id
	var err error
	if id > 0 {
		err = dao.UpdateById(ctx, artn)
	} else {
		id, err = dao.Insert(ctx, artn)
	}
	if err != nil {
		return 0, err
	}
	artn.Id = id
	now := time.Now().UnixMilli()
	pubArt := PublishedArticle(artn)
	pubArt.Ctime = now
	pubArt.Utime = now
	err = tx.Clauses(clause.OnConflict{
		Columns: []clause.Column{{Name: "id"}}, //对mysql不起效 可以兼容别的方言
		//INSERT XXX ON DUPLICATE KEY SET `title`=?
		//别的方言 sqlite INSERT XXX ON CONFLICT DO UPDATES
		//冲突了更新什么
		DoUpdates: clause.Assignments(map[string]interface{}{
			"title":   pubArt.Title,
			"content": pubArt.Content,
			"utime":   now,
		}),
	}).Create(&pubArt).Error
	if err != nil {
		return 0, err
	}
	tx.Commit() //提交后再回滚 不会影响
	return id, nil
}

type Article struct {
	Id      int64  `gorm:"primaryKey,autoIncrement" bson:"id,omitempty"`
	Title   string `gorm:"type=varchar(4096)" bson:"title,omitempty"`
	Content string `gorm:"type=BLOB" bson:"content,omitempty"`
	//根据创作者ID来查询
	AuthorId int64 `gorm:"index" bson:"author_id,omitempty"`
	Status   uint8 `bson:"status,omitempty"`
	Ctime    int64 `bson:"ctime,omitempty"`
	Utime    int64 `bson:"utime,omitempty"`
}

// 同库不同表 也可以用组合
type PublishedArticle Article
